/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { List } from "@kit.ArkTS"
import { socket } from "@kit.NetworkKit"
import { CipherSuite } from "./CipherSuite"
import { SSLSocket } from "./Socket/SSLSocket"
import { TlsVersion } from "./TlsVersion"
import { arraysEqual, arrayHashCode, binarySearch, hasIntersection, intersect, naturalOrder, stringHashCode } from "./utils/ArrayUtils"
import { kotlinRequire } from "./utils/Utils"

export class ConnectionSpec {
  #isTls: boolean
  get isTls(): boolean{
    return this.#isTls
  }

  #supportsTlsExtensions: boolean
  get supportsTlsExtensions(): boolean{
    return this.#supportsTlsExtensions
  }

  private cipherSuitesAsString?: Array<string>
  private tlsVersionsAsString?: Array<string>

  constructor(isTls: boolean, supportsTlsExtensions: boolean, cipherSuitesAsString?: Array<string>, tlsVersionsAsString?: Array<string>) {
    this.#isTls = isTls
    this.#supportsTlsExtensions = supportsTlsExtensions
    this.cipherSuitesAsString = cipherSuitesAsString
    this.tlsVersionsAsString = tlsVersionsAsString
  }

  /**
   * Returns the cipher suites to use for a connection. Returns null if all of the SSL socket's
   * enabled cipher suites should be used.
   */
  #cipherSuites?: List<CipherSuite>
  get cipherSuites(): List<CipherSuite> | null {
    if (this.cipherSuitesAsString === null || this.cipherSuitesAsString === undefined) {
      return null
    }
    const list = new List<CipherSuite>()
    this.cipherSuitesAsString.forEach(name => {
      list.add(CipherSuite.forJavaName(name))
    })
    return list
  }

  /**
   * Returns the TLS versions to use when negotiating a connection. Returns null if all of the SSL
   * socket's enabled TLS versions should be used.
   */
  #tlsVersions?: List<TlsVersion>
  get tlsVersions(): List<TlsVersion> | null {
    if (this.tlsVersionsAsString === null || this.tlsVersionsAsString === undefined) {
      return null
    }
    const list = new List<TlsVersion>()
    this.tlsVersionsAsString.forEach(name => {
      list.add(TlsVersion.forJavaName(name))
    })
    return list
  }

  /** Applies this spec to [sslSocket]. */
  apply(sslSocket: SSLSocket, isFallback: boolean) {
    let specToApply = this.supportedSpec(sslSocket, isFallback)

    if (specToApply.tlsVersions !== null && specToApply.tlsVersions !== undefined && 
        specToApply.tlsVersionsAsString !== null && specToApply.tlsVersionsAsString !== undefined) {
      sslSocket.enabledProtocols = specToApply.tlsVersionsAsString
    }

    if (specToApply.cipherSuites !== null && specToApply.cipherSuites !== undefined && 
        specToApply.cipherSuitesAsString !== null && specToApply.cipherSuitesAsString !== undefined) {
      sslSocket.enabledCipherSuites = specToApply.cipherSuitesAsString
    }
  }

  /**
   * Returns a copy of this that omits cipher suites and TLS versions not enabled by [sslSocket].
   */
  private supportedSpec(sslSocket: SSLSocket, isFallback: boolean): ConnectionSpec {
    let cipherSuitesIntersection: string[] = []
    if (this.cipherSuitesAsString !== null && this.cipherSuitesAsString !== undefined){
      cipherSuitesIntersection = intersect(sslSocket.enabledCipherSuites,this.cipherSuitesAsString, CipherSuite.ORDER_BY_NAME)
    }else{
      cipherSuitesIntersection = sslSocket.enabledCipherSuites
    }

    let tlsVersionsIntersection: string[] = []
    if (this.tlsVersionsAsString !== null && this.tlsVersionsAsString !== undefined) {
      tlsVersionsIntersection = intersect(sslSocket.enabledProtocols,this.tlsVersionsAsString, naturalOrder)
    } else {
      tlsVersionsIntersection = sslSocket.enabledProtocols
    }

    // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00 the SCSV
    // cipher is added to signal that a protocol fallback has taken place.
    let supportedCipherSuites = sslSocket.supportedCipherSuites
    let indexOfFallbackScsv = binarySearch(supportedCipherSuites,"TLS_FALLBACK_SCSV", CipherSuite.ORDER_BY_NAME)
    if (isFallback && indexOfFallbackScsv != -1) {
      cipherSuitesIntersection = cipherSuitesIntersection.concat(
        supportedCipherSuites[indexOfFallbackScsv])
    }

    return new ConnectionSpec.Builder(this)
      .cipherSuites(cipherSuitesIntersection)
    .tlsVersions(tlsVersionsIntersection)
    .build()
  }

  /**
   * Returns `true` if the socket, as currently configured, supports this connection spec. In
   * order for a socket to be compatible the enabled cipher suites and protocols must intersect.
   *
   * For cipher suites, at least one of the [required cipher suites][cipherSuites] must match the
   * socket's enabled cipher suites. If there are no required cipher suites the socket must have at
   * least one cipher suite enabled.
   *
   * For protocols, at least one of the [required protocols][tlsVersions] must match the socket's
   * enabled protocols.
   */
  isCompatible(socket: SSLSocket): boolean {
    if (!this.#isTls) {
      return false
    }

    if (this.tlsVersionsAsString !== null && this.tlsVersionsAsString !== undefined && 
        !hasIntersection(this.tlsVersionsAsString, socket.enabledProtocols)){
      return false
    }

    if (this.cipherSuitesAsString !== null && this.cipherSuitesAsString !== undefined && 
        !hasIntersection(this.cipherSuitesAsString, socket.enabledCipherSuites)){
      return false
    }

    return true
  }

  equals(other?: unknown): boolean {
    if (!(other instanceof ConnectionSpec)) return false
    if (other === this) return true

    if (this.#isTls !== other.#isTls) return false

    if (this.#isTls) {
      if (!arraysEqual(this.cipherSuitesAsString, other.cipherSuitesAsString)) return false
      if (!arraysEqual(this.tlsVersionsAsString, other.tlsVersionsAsString)) return false
      if (this.#supportsTlsExtensions !== other.#supportsTlsExtensions) return false
    }

    return true
  }

  hashCode(): number {
    let result = 17
    if (this.#isTls) {
      result = 31 * result + arrayHashCode(this.cipherSuitesAsString)
      result = 31 * result + arrayHashCode(this.tlsVersionsAsString)
      result = 31 * result + (this.#supportsTlsExtensions ? 0 : 1)
    }
    return result
  }

  toString(): string {
    if (!this.#isTls) return "ConnectionSpec()"

    const cipherSuitesStr = this.cipherSuites !== null && this.cipherSuites !== undefined
      ? `[${Array.from(this.cipherSuites).map(cs => (cs as CipherSuite).javaName).join(', ')}]`
      : "[all enabled]"
    
    const tlsVersionsStr = this.tlsVersions !== null && this.tlsVersions !== undefined
      ? `[${Array.from(this.tlsVersions).map(tv => (tv as TlsVersion).javaName).join(', ')}]`
      : "[all enabled]"

    return `ConnectionSpec(` +
      `cipherSuites=${cipherSuitesStr}, ` +
      `tlsVersions=${tlsVersionsStr}, ` +
      `supportsTlsExtensions=${this.#supportsTlsExtensions})`
  }

  static Builder = class Builder{
    #tls: boolean = false
    #cipherSuites?: Array<string> = undefined
    #tlsVersions?: Array<string> = undefined
    #supportsTlsExtensions: boolean = false

    constructor(tls: boolean | ConnectionSpec) {
      if (tls instanceof ConnectionSpec) {
        this.#tls = tls.isTls
        this.#cipherSuites = tls.cipherSuitesAsString
        this.#tlsVersions = tls.tlsVersionsAsString
        this.#supportsTlsExtensions = tls.supportsTlsExtensions
        return
      }
      this.#tls = tls
    }

    allEnabledCipherSuites(){
      kotlinRequire(this.#tls,"no cipher suites for cleartext connections")
      this.#cipherSuites = undefined
      return this
    }

    cipherSuites(cipherSuites: string[] | CipherSuite[]){
      kotlinRequire(this.#tls, "no cipher suites for cleartext connections")
      kotlinRequire(cipherSuites.length != 0, "At least one cipher suite is required")
      let cipherSuite: string[] = cipherSuites.map(it => {
        if (it instanceof CipherSuite) {
          return it.javaName
        }
        return it
      })
      this.#cipherSuites = cipherSuite
      return this
    }

    allEnabledTlsVersions(){
      kotlinRequire(this.#tls, "no TLS versions for cleartext connections")
      this.#tlsVersions = undefined
      return this
    }

    tlsVersions(tlsVersions: string[] | TlsVersion[]){
      kotlinRequire(this.#tls, "no TLS versions for cleartext connections")
      kotlinRequire(tlsVersions.length != 0, "At least one TLS version is required")

      let tlsVersion: string[] = tlsVersions.map(it => {
        if (it instanceof TlsVersion) {
          return it.javaName
        }
        return it
      })
      this.#tlsVersions = tlsVersion
      return this
    }

    supportsTlsExtensions(supportsTlsExtensions: boolean){
      kotlinRequire(this.#tls, "no TLS extensions for cleartext connections")
      this.#supportsTlsExtensions = supportsTlsExtensions
      return this
    }

    build(): ConnectionSpec {
      return new ConnectionSpec(
        this.#tls,
        this.#supportsTlsExtensions,
        this.#cipherSuites,
        this.#tlsVersions
      )
    }
  }
  // Most secure but generally supported list.
  private static RESTRICTED_CIPHER_SUITES: Array<CipherSuite> = [
    // TLSv1.3.
    CipherSuite.TLS_AES_128_GCM_SHA256,
    CipherSuite.TLS_AES_256_GCM_SHA384,
    CipherSuite.TLS_CHACHA20_POLY1305_SHA256,

    // TLSv1.0, TLSv1.1, TLSv1.2.
    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
    CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
    CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
    CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
    CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
  ]

  // This is nearly equal to the cipher suites supported in Chrome 72, current as of 2019-02-24.
  // See https://tinyurl.com/okhttp-cipher-suites for availability.
  private static APPROVED_CIPHER_SUITES: Array<CipherSuite> = [
    // TLSv1.3.
    CipherSuite.TLS_AES_128_GCM_SHA256,
    CipherSuite.TLS_AES_256_GCM_SHA384,
    CipherSuite.TLS_CHACHA20_POLY1305_SHA256,

    // TLSv1.0, TLSv1.1, TLSv1.2.
    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
    CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
    CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
    CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
    CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
    CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,

    // Note that the following cipher suites are all on HTTP/2's bad cipher suites list. We'll
    // continue to include them until better suites are commonly available.
    CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
    CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
    CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,
    CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384,
    CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
    CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
    CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA
  ]

  /** Unencrypted, unauthenticated connections for `http:` URLs. */
  static CLEARTEXT = new ConnectionSpec.Builder(false).build()

  /** A secure TLS connection that requires a recent client platform and a recent server. */
  static RESTRICTED_TLS = new ConnectionSpec.Builder(true)
    .cipherSuites(ConnectionSpec.RESTRICTED_CIPHER_SUITES)
    .tlsVersions([TlsVersion.TLS_1_3, TlsVersion.TLS_1_2])
    .supportsTlsExtensions(true)
    .build()

  /**
   * A modern TLS configuration that works on most client platforms and can connect to most servers.
   * This is OkHttp's default configuration.
   */
  static MODERN_TLS = new ConnectionSpec.Builder(true)
    .cipherSuites(ConnectionSpec.APPROVED_CIPHER_SUITES)
    .tlsVersions([TlsVersion.TLS_1_3, TlsVersion.TLS_1_2])
    .supportsTlsExtensions(true)
    .build()

  /**
   * A backwards-compatible fallback configuration that works on obsolete client platforms and can
   * connect to obsolete servers. When possible, prefer to upgrade your client platform or server
   * rather than using this configuration.
   */
  static COMPATIBLE_TLS = new ConnectionSpec.Builder(true)
    .cipherSuites(ConnectionSpec.APPROVED_CIPHER_SUITES)
    .tlsVersions([TlsVersion.TLS_1_3, TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0])
    .supportsTlsExtensions(true)
    .build()

}